/*
 * Unidata Platform Community Edition
 * Copyright (c) 2013-2020, UNIDATA LLC, All rights reserved.
 * This file is part of the Unidata Platform Community Edition software.
 *
 * Unidata Platform Community Edition is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Unidata Platform Community Edition is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 */
package org.unidata.mdm.meta.service.impl.data.refresh;

import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.unidata.mdm.core.context.ModelRefreshContext;
import org.unidata.mdm.core.type.model.EntityElement;
import org.unidata.mdm.core.type.model.RelationElement;
import org.unidata.mdm.core.util.SecurityUtils;
import org.unidata.mdm.meta.configuration.Descriptors;
import org.unidata.mdm.meta.service.impl.data.DataModelMappingComponent;
import org.unidata.mdm.meta.service.impl.data.instance.LookupImpl;
import org.unidata.mdm.meta.service.impl.data.instance.RegisterImpl;
import org.unidata.mdm.meta.service.impl.data.instance.RelationImpl;
import org.unidata.mdm.meta.type.instance.DataModelInstance;
import org.unidata.mdm.meta.type.search.EntityIndexType;
import org.unidata.mdm.meta.type.search.RelationHeaderField;
import org.unidata.mdm.search.context.MappingRequestContext;
import org.unidata.mdm.search.context.SearchRequestContext;
import org.unidata.mdm.search.service.SearchService;
import org.unidata.mdm.search.type.form.FieldsGroup;
import org.unidata.mdm.search.type.form.FormField;
import org.unidata.mdm.search.type.query.SearchQuery;

/**
 * @author Mikhail Mikhailov on Nov 2, 2020
 */
@Component
@Order(Ordered.LOWEST_PRECEDENCE)
public class MappingUpdatesRefreshListener extends AbstractDataModelRefreshListener {
    /**
     * The search service.
     */
    @Autowired
    private SearchService searchService;
    /**
     * The mapping service.
     */
    @Autowired
    private DataModelMappingComponent dataModelMappingComponent;

    /**
     * Constructor.
     */
    public MappingUpdatesRefreshListener() {
        super();
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public void refresh(ModelRefreshContext refresh) {

        String storageId = SecurityUtils.getStorageId(refresh);
        DataModelInstance fresh = metaModelService.instance(Descriptors.DATA, storageId, null);

        int revision = fresh.getVersion();
        boolean isNew = revision == 1;

        List<EntityElement> deletedEntities;
        List<RelationElement> deletedRelations;
        List<EntityElement> updatedEntities;
        List<RelationElement> updatedRelations;

        // Just create the stuff needed
        if (isNew) {
            deletedEntities = Collections.emptyList();
            deletedRelations = Collections.emptyList();
        // Do it the hard way - fetch prev from DB to overcome a possible revision gap
        // Calc diff and run updates
        } else {

            DataModelInstance p = getPreviousState(refresh);

            deletedEntities
                = Stream.concat(p.getRegisters().stream().filter(r -> Objects.isNull(fresh.getRegister(r.getName()))),
                                p.getLookups().stream().filter(l -> Objects.isNull(fresh.getLookup(l.getName()))))
                    .collect(Collectors.toList());

            deletedRelations
                = p.getRelations().stream().filter(r -> Objects.isNull(fresh.getRelation(r.getName())))
                    .collect(Collectors.toList());
        }

        updatedEntities
            = Stream.concat(fresh.getRegisters().stream().filter(r -> ((RegisterImpl) r).getSource().getVersion() == fresh.getVersion()),
                            fresh.getLookups().stream().filter(l -> ((LookupImpl) l).getSource().getVersion() == fresh.getVersion()))
                .collect(Collectors.toList());

        updatedRelations = fresh.getRelations().stream()
                .filter(r -> ((RelationImpl) r).getSource().getVersion() == fresh.getVersion())
                .collect(Collectors.toList());

        // 1. Update model index
        updateModelIndex(deletedEntities, deletedRelations, updatedEntities, updatedRelations);

        // 2. Update data mappings
        updatedEntities.forEach(el -> dataModelMappingComponent.updateEntityMappings(storageId, false, el.getName()));
        updatedRelations.forEach(el -> dataModelMappingComponent.updateRelationMappings(storageId, null, el.getName()));
        deletedEntities.forEach(el -> searchService.dropIndex(MappingRequestContext.builder()
                .drop(true)
                .entity(el.getName())
                .storageId(storageId)
                .build()));

        for (RelationElement el : deletedRelations) {

            if (fresh.isRegister(el.getLeft().getName())) {
                dropAllIndexedRels(el.getName(), el.getLeft().getName(), storageId);
            }

            if (fresh.isRegister(el.getRight().getName())) {
                dropAllIndexedRels(el.getName(), el.getRight().getName(), storageId);
            }
        }
    }

    private void updateModelIndex(
            List<EntityElement> deletedEntities,
            List<RelationElement> deletedRelations,
            List<EntityElement> updatedEntities,
            List<RelationElement> updatedRelations) {

        dataModelMappingComponent.removeFromMetaModelIndex(
                Stream.concat(deletedEntities.stream(), deletedRelations.stream())
                    .map(EntityElement::getName)
                    .toArray(String[]::new));

        for (EntityElement el : updatedEntities) {

            if (!el.isSearchable()) {
                continue;
            }

            dataModelMappingComponent.removeFromMetaModelIndex(el.getName());
            dataModelMappingComponent.putToMetaModelIndex(el.getSearchable().getModelSearchEntries());
        }

        for (EntityElement el : updatedRelations) {

            if (!el.isSearchable()) {
                continue;
            }

            dataModelMappingComponent.removeFromMetaModelIndex(el.getName());
            dataModelMappingComponent.putToMetaModelIndex(el.getSearchable().getModelSearchEntries());
        }
    }

    private void dropAllIndexedRels(String relName, String entityName, String storageId) {

        SearchRequestContext context = SearchRequestContext.builder(EntityIndexType.RELATION, entityName)
                .storageId(storageId)
                .query(SearchQuery.formQuery(FieldsGroup.and(FormField.exact(RelationHeaderField.FIELD_RELATION_NAME, relName))))
                .build();

        searchService.deleteFoundResult(context);
    }
}
